氣味的徵兆
相似於我們上一篇所介紹的長方法(Long Method)氣味,「大類別(Large Class)」顧名思義,是指隨著時間累積,開發者不斷疊加新功能與商業邏輯在單一類別上,最終形成一個尺寸超乎合理範圍的龐然大物。
開發者在每一次提交程式碼更動(Pull Request)進行程式碼審查(Code Review)時,這些新增需求的改動看上去都堪稱合理。畢竟依照git diff所提供的程式碼變動差異觀點,我們習慣性只會聚焦在新增加的行數之上,卻容易忽略了從專案的整體全局視角,去更進一步深入檢視整個類別在新增本次改動之後,是否依然能夠維持在「好的氣味(Good Smell)」範疇之內。
這也是我在職業上累積的心得之一:許多團隊只注重個別程式碼提交的審查(Code Review per each PRs),但卻忽視了以「發佈為範圍的批次程式碼審核」(Code Review per Sprint)的重要性。各別單獨看上去合理的程式碼改動,一旦加上更多更完整的上下文與脈絡重新檢視以後,不見得依舊合理,或是我們能夠找出更好的改進方案。
不同語言雖然存在典範差異,但通常來說,當單一類別的檔案長度已經超過500行時,開發者最好懷疑這個類別是否已經過於巨大、擔負過多職責了。
氣味的原因
同為尺寸過大造成的影響, Large Class 部分的病灶自然和 Long Method 有重疊之處。但考慮到獨立訪問的讀者(並不是每一位讀者都會閱讀整個系列文章),每一種程式碼氣味的介紹內容無法像是「類別」一樣透過「繼承」取得,尚且容許我在此重複提及部分片段。
-
可讀性低:無庸置疑的,一個大類別需要開發者花上更多時間去閱讀與理解。開發者光是從頭滑到檔案的最底部就需要花上不少時間,更別提要徹底清楚明白目前的類別中存在哪些方法與屬性,需要花費更高的學習成本。
-
重複代碼的可能性高:大類別有可能存在開發者為了省時間,而大量的「複製貼上」相似功能的程式碼片段,而沒有仔細考慮抽象之後共用的設計模式,以及如何重複使用現有的功能。在一個很極端的例子,我甚至曾經在重構時發現一個巨型的類別中存在功能完全相同的兩個方法,只因為由不同開發者在不同時間為了相同原因所寫,而後者並不知道專案內早已經存在所需要的方法來完成任務。
-
可能違反高內聚(High Cohesion)原則:「高內聚、低耦合(High Cohesion, Low Coupling)」是軟體開發者之間耳熟能詳的指導原則,而大類別很可能違反其中的「高內聚」。當類別越大,其中可能存在更多相關程度不高的功能或職責,而降低了「內聚性(Cohesion)」。低內聚的類別可能導致不易維護、不容易使用,並且不容易理解其意圖。
-
維護管理複雜性:大類別通常比起小類別更加複雜,這代表開發者維護管理的困難度也提高,需要更多時間才能夠徹底了解。高複雜性的類別更容易導致錯誤,並且增加新開發者加入團隊時所需要面對的挑戰難度與額外學習成本。
-
不可預期性:當大量的不同職責與任務集中在巨大的類別內時,當我們修改時容易顧此失彼,影響到非相關的元件。這些副作用(Side Effects)會讓每次改動程式的風險提高,並且難以保持具有一致性可預測的結果。
-
測試困難度增加:測試一個巨大的類別需要耗費更多時間。不僅是執行時需要更多時間,在閱讀測試、撰寫測試、維護測試上,以及考慮不同的邏輯路徑等,都會讓測試的困難度大大提升。
總結而言,若以「乾淨的程式碼(clean code)」為目標,大就是一種原罪。如同 Sandi 在「All the Little Things」中所揭示的秘訣,我們應該盡可能的撰寫出小的類別、小的方法,藉此來提升閱讀與使用上的效率。
重構手法
為了將大類別化繁為簡,開發者可以參考以下幾種重構手法,後方括號為該重構技巧的分類。
- Extract Class (Moving Features between Objects)
- Extract Subclass (Dealing with Generalization)
- Extract Interface (Dealing with Generalization)
- Replace Data Value with Object (Organizing Data)
- Duplicate Observed Data (Organizing Data)
- Replace Conditional Dispatcher with Command (Simplifying Conditional Expressions)
- Replace Implicit Language with Interpreter
- Replace State-Altering Conditionals with State
原本想將重構手法與氣味介紹整合在同一篇文章內,但實際撰寫時依然覺得太多(同時也是無法在期限內完成文章),所以一樣會在下一篇分享。
Sign of Smell
Similar to the “Long Method” smell, a class always starts small and grows over time as developers keep adding more features to it. However, as the class grows, it becomes increasingly difficult to read and understand, and it becomes more challenging to make changes to it.
The Large Class smell is a code smell that occurs when a single class in a software system becomes overly large and complex, often trying to do too many things at once. When a class has too many responsibilities, it can result in a bloated class with duplicate code, chaos, and death. This makes class harder to maintain and refactor.
Based on my personal experience, if you come across a class with more than 500 lines, be suspicious that it may be too large.
Reason of Smell
-
Reduced Readability: A large class can be overwhelming to read and understand. Developers may have to scroll through a massive file to comprehend its functionality, making it harder to maintain or modify the code later on.
-
Code Duplication: Large classes often result in code duplication because developers might be tempted to copy-paste code within the class rather than create smaller, reusable components.
-
Low Cohesion: Large classes tend to have low cohesion, meaning they contain multiple unrelated responsibilities and functionalities. This makes it challenging to determine the class's main purpose and can lead to confusion about which methods and attributes are related.
-
Complexity Management: Large classes are more likely to be complex, making it harder to manage and reason about the code. The complexity can lead to bugs and make it challenging for new developers to onboard and contribute effectively.
-
Brittleness: When many responsibilities are grouped together in a single class, modifying one part of a large class can unexpectedly impact other unrelated sections and easily lead to side effects, making it hard to predict the consequences of changes.
-
Testing Challenges: Testing large classes can be cumbersome and time-consuming. Writing comprehensive unit tests for a class that does many things can be difficult and may not cover all possible scenarios effectively.
Refactoring Recipe
To address this issue, developers can use several refactoring techniques. One approach is to extract a part of the Large Class into a new class, thereby reducing the complexity of the original class. This technique, known as Extract Class, allows for more focused and specialized classes, which are easier to understand and maintain. Other techniques include Extract Subclass, Extract Interface, Replace Data Value with Object, Replace Conditional Dispatcher with Command, Replace Implicit Language with Interpreter, Replace State-Altering Conditionals with State, and Duplicate Observed Data.
By addressing the "Large Class" smell, developers can improve the quality of their code and make it more maintainable, extensible, and readable.
- Extract Class (Moving Features between Objects)
- Extract Subclass (Dealing with Generalization)
- Extract Interface (Dealing with Generalization)
- Replace Data Value with Object (Organizing Data)
- Duplicate Observed Data (Organizing Data)
- Replace Conditional Dispatcher with Command (Simplifying Conditional Expressions)
- Replace Implicit Language with Interpreter
- Replace State-Altering Conditionals with State
Reference
https://refactoring.guru/smells/large-class